Skip to main content

07 · 代码即行动:把动作写成代码

前置:01 ReAct。本篇是对 01 的一次替换 —— 同一个循环,换掉「动作」的表达方式。

关于名字:这个范式学术上叫 CodeAct(出自 arXiv:2402.01030),但工业界几乎不用这个词 —— HuggingFace 叫 Code Agents,Anthropic 叫 code execution with MCP,Cloudflare 叫 Code Mode,各家一个叫法。

所以你很可能见过实物却没听过名字:Claude Code 主要靠 bash 干活、Manus 在沙箱里写脚本,走的都是这条路。

一、问题:JSON 表达不了组合

01 里模型的动作长这样:

{"name": "bash", "input": {"command": "find . -name '*.py'"}}

一次一个工具,一个参数字典。现在给它一个稍复杂的任务:

「在这三个网站上分别搜同一个关键词,把结果去重后取前十条。」

用 JSON 工具调用,模型要跑至少 5 轮:搜 A → 搜 B → 搜 C → 拿到三份结果 → 再想办法合并。而「去重取前十」这件事,JSON 根本没法表达 —— 模型只能把三份原始结果全部读进上下文,靠自己心算。

同样的事写成代码是一轮:

JSON 动作:5 轮,且「去重取前十」无处安放

代码动作:1 轮,合并逻辑由解释器执行

results = []
for url in ["a.com", "b.com", "c.com"]:
results += web_search(url, "关键词")
final = sorted(set(results))[:10]
print(final)

差别不在于代码更短,而在于JSON 缺三样东西

缺什么具体表现
组合你没法把一个 JSON 动作嵌套进另一个,也没法定义一组动作复用
对象管理generate_image 返回一张图,JSON 怎么存住它给下一步用?
控制流循环、条件、异常处理,JSON 里全都没有

而这三样恰恰是编程语言被发明出来要解决的事。smolagents 官方文档把这个论点说得最直白:

我们的 Agent 要写程序来解决用户的问题:你觉得用 Python 写更容易,还是用 JSON 写更容易?

二、CodeAct 是什么

同一个 ReAct 循环,只换动作的表达形式:

01 ReAct(ToolCalling)CodeAct
模型输出结构化的 tool_use一段 Python 代码
谁解析API 直接给你结构化对象你要从自由文本里抠出代码块
谁执行你的 if name == "bash" 分发Python 解释器
Observation工具的返回值代码的 stdout
一轮能做几件事一次一个(或并行几个独立的)任意组合,含循环和条件

论文依据是 Wang 等的 Executable Code Actions Elicit Better LLM AgentsarXiv:2402.01030):同样的任务,代码动作比 JSON 动作少用约 30% 的步数 —— 步数少即模型调用次数少。

Code actions vs JSON actions

图 7-1 代码动作与 JSON 动作的对比(出自 arXiv:2402.01030)

图片来源:smolagents 官方文档(Apache-2.0)

它不是「让 Agent 帮你写代码」。 那是任务内容;CodeAct 说的是动作的编码格式。一个做数据分析的 CodeAct Agent,写的代码是它自己的动作,用户根本看不到。

三、源码:smolagents 的 CodeAgent

huggingface/smolagents(28,875★,Apache-2.0)的 README 第一条特性就是「First-class support for Code Agents」。agents.pyCodeAgentToolCallingAgent 是并列的两个类,共享同一个 MultiStepAgent 基类 —— 这正说明两者只差在动作格式上

3.1 一轮里发生了什么

### 解析输出 ###
code_action = parse_code_blobs(output_text, self.code_block_tags)
code_action = fix_final_answer_code(code_action)

### 执行动作 ###
code_output = self.python_executor(code_action)
observation = "Execution logs:\n" + code_output.logs
observation += "Last output from code snippet:\n" + truncate_content(str(code_output.output))

四行里有三个关键点:

parse_code_blobs —— 解析责任转移到了你身上

ReAct 里 API 直接返回结构化的 tool_use 块,格式由协议保证。CodeAct 里模型吐的是自由文本,你得自己从 ```py ... ``` 里把代码抠出来。这是 CodeAct 多出来的第一份工程成本。

observation = "Execution logs:" + logs —— stdout 就是 Observation

这是和 01 最本质的差别。ReAct 的 Observation 是工具返回值;CodeAct 的 Observation 是代码打印出来的东西

推论很实际:模型必须记得 print(),否则它什么都看不到。 代码正确执行但忘了打印,这一轮就白跑了。所以 CodeAct 的系统提示词里必然有一条「记得把要观察的结果打印出来」。

注意它还额外带回了 code_output.output(最后一个表达式的值),这是给忘了 print 的情况留的兜底。

③ 一个诱导模型停止生成的小技巧

# 把结束标签补进历史,诱导后续调用也以它结尾,从而高效地停止生成
if output_text and not output_text.strip().endswith(self.code_block_tags[1]):
output_text += self.code_block_tags[1]

模型没写收尾的 ``` 时,框架替它补上再存进历史。下一轮模型看到历史里每段代码都以它结尾,就会照做 —— 用历史的格式一致性来控制生成的停止位置,比调 stop sequence 更省事。

3.2 安全:白名单,不是黑名单

回想 01 的 6.5 节,玩具版用的是关键词黑名单,rm -rf /* 就能绕过。CodeAct 因为直接跑 Python,风险面更大,所以做法完全不同:

self.authorized_imports = sorted(set(BASE_BUILTIN_MODULES) | set(self.additional_authorized_imports))

能 import 什么是白名单,默认只有一组基础模块。 想让 Agent 用 pandas,你得显式加进 additional_authorized_imports

而且失败时给的是可操作的错误 —— 又一次印证 08 的 4.1 条

if "Import of " in error_msg and " is not allowed" in error_msg:
self.logger.log(
"Warning to user: Code execution failed due to an unauthorized import - "
"Consider passing said import under `additional_authorized_imports` ...")

"*" 可以放开全部,框架会打一条警告日志。别在生产里这么干。

沙箱是一等公民参数,不是事后补的:

executor_type: Literal["local", "blaxel", "e2b", "modal", "docker"] = "local"

默认 local 只适合你自己机器上跑着玩。只要代码来源是模型,生产就必须换成远程沙箱或 Docker。 这也是为什么 CodeAgent 实现了 __enter__/__exit__/cleanup —— 远程执行器用完要回收。

3.3 变量在轮次之间存活

self.state.update(additional_args)
self.python_executor.send_variables(variables=self.state)

这是 CodeAct 独有、ReAct 拿不到的能力:第 1 轮定义的变量,第 5 轮还能直接用。

# 第 1 轮
df = pd.read_csv("data.csv") # 十万行,留在解释器内存里
print(df.shape) # Observation 只有 "(100000, 12)"

# 第 4 轮
print(df.groupby("city").size()) # 直接用,不必重新读盘、更不必进上下文

大对象留在解释器里,只有你 print 的摘要进上下文。 这是第一节说的「对象管理」在实现层面的样子,也是 CodeAct 在数据分析类任务上优势明显的原因。

对照 01 ReAct:那里工具返回什么,什么就原样进上下文 —— 一个大 DataFrame 会直接把窗口撑爆。

四、生产采用情况

怎么用依据
smolagentsCodeAgent 是官方首推形态README「First-class support for Code Agents」
Anthropic让模型写代码调 MCP 工具,而非逐个 JSON 调用Code execution with MCP
CloudflareCode Mode:把 MCP 工具转成 TypeScript API 交给模型写Code Mode
Manus沙箱内执行代码作为主要动作方式官方博客与泄露的工具清单

Anthropic 那篇的论点值得单独记:工具一多,把几十个工具的完整定义塞进上下文本身就很贵;改成让模型写代码去调,工具定义可以按需加载。这是 CodeAct 在工具数量多时的第二个收益 —— 省的不是步数,是上下文。

五、代价

CodeAct 不是免费的升级,它把三样成本转移给了你:

成本说明
必须有沙箱ReAct 的工具是你写的、边界你定;CodeAct 执行的是模型现写的代码。没有沙箱就是把 exec() 交给模型
解析变脆从自由文本抠代码块,模型格式跑偏就解析失败。ReAct 有 API 协议保证
调试变难ReAct 报错定位到某个工具;CodeAct 的报错是一段 traceback,可能是模型逻辑错,也可能是工具本身错

还有一个隐性门槛:弱模型写不好代码。JSON 工具调用只要求填对字段,写代码要求真正的编程能力 —— 模型能力不够时,CodeAct 反而更差。

六、故障排查

现象原因修法
每轮都成功但 Agent 学不到东西代码跑了但没 print,Observation 是空的提示词强调打印;用 code_output.output 兜底
频繁 import 失败白名单没放行加进 additional_authorized_imports,别用 "*"
解析不到代码块模型没按 ```py ``` 格式输出补结束标签(3.1 ③);或开结构化输出
一次执行拖很久模型写了死循环或大规模计算执行器超时;换远程沙箱限制资源
上下文被 traceback 撑爆报错原样回传截断 traceback,只留最后几帧
换了小模型后效果暴跌编程能力不足退回 01 ReAct 的 JSON 工具调用

七、定位与边界

CodeAct 和 01 ReAct 占的是同一个格子(认知=Action,拓扑=Loop,见 08 第一节)—— 它不是新的编排结构,是同一结构的另一种动作编码。所以它能和其他范式自由叠加:CodeAct + Plan、CodeAct 子代理 + Supervisor 都成立。

该用:动作需要组合、循环、条件;要处理不该进上下文的大对象(DataFrame、图像);工具数量多到定义本身撑爆上下文。

不该用:只有两三个简单工具(JSON 更稳);跑不了沙箱的环境;模型编程能力弱;要严格审计每一个动作(代码比 JSON 难做细粒度权限控制)。

参考资料

资料位置协议
主拆源码smolagents/src/smolagents/agents.pyCodeAgent,1813 行文件Apache-2.0
论文arXiv:2402.01030 Executable Code Actions Elicit Better LLM Agents——
论文源码xingyaoww/code-act,1,698★(2024-05 停更)MIT
工具定义按需加载Anthropic — Code execution with MCP——
另一种落地Cloudflare — Code Mode——

下一篇:08 · 横向对比与选型